// I'LL KNOW, WE KNOW, JAVASCRIPT IT'S VERY FUCK
import "../../lib/ramda.js";
import game from "./game.js";
import init, {
    sslash_init,sslash_step,sslash_set,sslash_gc
} from "./pkg/sslash.js";

const BOARD_LEN = game.BOARD_LEN
const P_BLACK = game.PIECE_BLACK
const P_WHITE = game.PIECE_WHITE
const N_UCT_TIMES_MIN = 1024
const N_UCT_TIMES_MAX = 1048576
const N_LEVEL_MIN = 0
const N_LEVEL_MAX = 9
const N_SERARCH_DEPTH = 3

const ZOBRIST_LEN = BOARD_LEN*3
const ZOBRIST = new Uint32Array(ZOBRIST_LEN)
for (let i = 0; i < ZOBRIST_LEN; i++){
    ZOBRIST[i] = Math.floor(Math.random() * 4294967295)
}

function zobrist_code(arr) {
    let code = 0
    for (let i = 0; i < BOARD_LEN; i++){
        const j = ((1 + arr[i]) * BOARD_LEN) + i
        code ^= ZOBRIST[j]
    }
    return code
}

class Board {
    constructor(_from=undefined) {
        if (R.is(Board,_from)) {
            this._board = new game.Board(_from._board)
            this._hash = _from.hash
        } else {
            this._board = new game.Board(_from)
            this._hash = zobrist_code(this._board._data())
        }
    }
    n_black() {
        return this._board.n_black()
    }
    n_white() {
        return this._board.n_white()
    }
    hash() {
        return this._hash
    }
    see(i) {
        return this._board.see(i)
    }
    can_move(pcur, loc) {
        return this._board.can_move(pcur, loc)
    }
    moves(pcur) {
        return this._board.moves(pcur)
    }
    down(pcur, i, _can_down=false) {
        if (this._board.down(pcur, i , _can_down)) {
            this._hash = zobrist_code(this._board._data())
        }
    }
    reset(_from) {
        if (R.is(Board,_from)) {
            this._board = new game.Board(_from._board)
            this._hash = _from.hash
        }
    }
}

const ENGINE_USING_LOCAL = 2;
const ENGINE_USING_WASM = 1;
const ENGINE_USING_JS = 0;
const State = {
    engine_using: ENGINE_USING_JS,
    local_port: undefined,
    local_ws: undefined,
    local_takes: [],
    n_uct_times: 0,
    n_level: 0,
}

const Out = {
    reply(...args){
        self.postMessage(args.join(' '))
    },
    reply_ok(...args){
        Out.reply("ok", ...args)
    },
    report_error(msg){
        self.postMessage(`[ERROR] ${msg}`)
    },
}

function rand_range(a,b){
    return Math.floor(Math.random() * (b-a)) + a
}

// Black MIN <-- Draw 0 --> White MAX
const VALUATION_MIN = Number.MIN_SAFE_INTEGER
const VALUATION_MAX = Number.MAX_SAFE_INTEGER
const VALUATION_LOCS = [
    512  ,-256 ,128  ,3    ,3    ,3    ,3    ,128  ,-256 ,512,
    -256 ,-256 ,16   ,2    ,2    ,2    ,2    ,16   ,-256 ,-256,
    128  ,16   ,128  ,1    ,1    ,1    ,1    ,128  ,16   ,128,
    3    ,2    ,1    ,1    ,1    ,1    ,1    ,1    ,2    ,3,
    3    ,2    ,1    ,1    ,1    ,1    ,1    ,1    ,2    ,3,
    3    ,2    ,1    ,1    ,1    ,1    ,1    ,1    ,2    ,3,
    3    ,2    ,1    ,1    ,1    ,1    ,1    ,1    ,2    ,3,
    128  ,16   ,128  ,1    ,1    ,1    ,1    ,128  ,16   ,128,
    -256 ,-256 ,16   ,2    ,2    ,2    ,2    ,16   ,-256 ,-256,
    512  ,-256 ,128  ,3    ,3    ,3    ,3    ,128  ,-256 ,512,
]
function valuation_whole(board) {
    const n_black = board.n_black()
    const n_white = board.n_white()
    if (n_black + n_white >= BOARD_LEN) {
        if (n_black == n_white) {
            return 0
        } else if (n_black < n_white) {
            return VALUATION_MAX
        } else {
            return VALUATION_MIN
        }
    } else if (n_black > 0 && n_white < 1) {
        return VALUATION_MIN
    } else if (n_white > 0 && n_black < 1) {
        return VALUATION_MAX
    } else {
        let corner_score = 0
        for (const loc in VALUATION_LOCS) {
            let p = board.see(loc)
            if (p == P_BLACK) {
                corner_score -= VALUATION_LOCS[loc]
            } else if (p == P_WHITE) {
                corner_score += VALUATION_LOCS[loc]
            }
        }
        const diff = n_white - n_black
        return diff * Math.abs(diff) + corner_score
    }
}

let HASHS_MOVES = {}
function hashs_moves(board, pcur) {
    const hash = board.hash()
    const item_id = (pcur == P_BLACK) ? 0 : 1
    if (hash in HASHS_MOVES) {
        const moves = HASHS_MOVES[hash][item_id]
        if (moves.length > 0) {return moves}
    }
    const _moves = [[],[]]
    for (let loc of R.range(0, game.BOARD_LEN)) {
        if (board.can_move(P_BLACK, loc)) {
            _moves[0].push(loc)
        } else if (board.can_move(P_WHITE, loc)) {
            _moves[1].push(loc)
        }
    }
    HASHS_MOVES[hash] = _moves
    return _moves[item_id]
}

let HASHS_N_PIECES = {}
function hashs_n_pieces(board, pcur) {
    const hash = board.hash()
    const item_id = ((pcur == P_BLACK) ? 0 : 1)
    if (hash in HASHS_N_PIECES) {
        return HASHS_N_PIECES[hash][item_id]
    }
    const _n_pieces = [board.n_black(), board.n_white()]
    HASHS_N_PIECES[hash] = _n_pieces
    return _n_pieces[item_id]
}

let HASHS_VALUATIONS = {}
function hashs_valuation(board){
    const hash = board.hash()
    if (hash in HASHS_VALUATIONS) {
        return HASHS_VALUATIONS[hash]
    }
    const whole = valuation_whole(board)
    HASHS_VALUATIONS[hash] = whole
    return whole
}

function hashs_gc() {
    HASHS_MOVES = {}
    HASHS_N_PIECES = {}
    HASHS_VALUATIONS = {}
}

function moves_shuffle(moves) {
    if (moves.length > 1) {
        for (let i = 1; i < moves.length; i++) {
            if (Math.random() > 0.5) {
                const a = moves[i]
                moves[i] = moves[0]
                moves[0] = a
            }
        }
    }
    return moves
}

class Counter {
    constructor() {
        this.reset()
    }
    reset() {
        this.n_node = 0
        this.n_pur = 0
    }
    add_node(n=1) {
        this.n_node += n
        return this
    }
    add_pur(n=1) {
        this.n_pur += n
        return this
    }
    report() {
        if ((this.n_node % 512) == 0) {
            Out.reply("status", `Node ${this.n_node}`)
        }
        return this
    }
}

function search(counter, pcur, board, depth, alpha=Number.MIN_SAFE_INTEGER, beta=Number.MAX_SAFE_INTEGER) {
    counter.report()
    if (depth > 0) {
        const moves = hashs_moves(board, pcur)
        if (moves.length > 0) {
            const node_board = new Board(board)
            for (const loc of moves_shuffle(moves)) {
                node_board.down(pcur, loc, true)
                counter.add_node()
                const next_value = search(counter, -pcur, node_board, depth-1, alpha, beta)
                if ((pcur == P_WHITE) && (next_value > alpha)) {
                    alpha = next_value
                }
                if ((pcur == P_BLACK) && (next_value < beta)) {
                    beta = next_value
                }
                if (alpha >= beta) {counter.add_pur(); break}
                node_board.reset(board)
            }

        } else {
            return search(counter, -pcur, board, depth-1, alpha, beta)
        }
        return (pcur == P_BLACK) ? beta : alpha
    }
    return hashs_valuation(board)
}

function step(pcur, board, uct_max_times, level) {
    const depth = Math.min(Math.max(N_SERARCH_DEPTH, 1 + Math.round(Math.log10(uct_max_times))), 16)
    const init_moves = board.moves(pcur)
    if (init_moves.length > 1) {
        const counter = new Counter()
        const values = init_moves.map((loc)=>{
            const node_board = new Board(board)
            node_board.down(pcur, loc, true)
            return search(counter, -pcur, node_board, depth)
        })
        let min_i = 0
        let max_i = 0
        for (const i in values) {
            if (values[i] > values[max_i]) {
                max_i = i
            } else if (values[i] < values[min_i]) {
                min_i = i
            }
        }
        for (const i in values) {
            console.log(init_moves[i], values[i])
        }
        const hit_i = (pcur == P_BLACK) ? min_i : max_i
        console.log(`MIN[${min_i}]=${values[min_i]} MAX[${max_i}]=${values[max_i]} HIT[${hit_i}]=${values[hit_i]}`)
        return [init_moves[hit_i]]
    }
    return init_moves
}

function sync_sets(fn_set) {
    fn_set("uct-times", State.n_uct_times)
    fn_set("level", State.n_level)
}

function local_take_push(msg) {
    State.local_takes = [msg, ...State.local_takes]
}
function local_take_poll(msg) {
    if (State.local_takes.length > 0) {
        return State.local_takes.pop()
    } else {
        return ""
    }
}
function local_take_clear() {
    State.local_takes = []
}
function local_reset() {
    if (R.is(WebSocket, State.local_ws)) {
        State.local_ws.close()
        State.local_ws = undefined
    }
}
function local_usable() {
    if (R.is(WebSocket, State.local_ws)) {
        return State.local_ws.readyState == WebSocket.OPEN
    } else {
        return false
    }
}
function local_link(port, fn_opened, fn_unlinked) {
    if (R.isNil(port)) {return}
    if (R.is(WebSocket, State.ws)) {return}
    const addr = `ws://localhost:${port}`
    const ws = new WebSocket(addr)
    ws.addEventListener("open", ()=>{
        console.log("[LOCAL] linked")
        local_take_clear()
        State.local_port = port
        State.local_ws = ws
        setTimeout(()=>{sync_sets(local_set)}, 100)
        function send_heartbeat() {
            if (ws.readyState == WebSocket.OPEN) {
                ws.send("heartbeat")
                setTimeout(send_heartbeat, 2000)
            }
        }
        setTimeout(send_heartbeat, 2000)
        fn_opened()
    })
    ws.addEventListener("close", (e)=>{
        console.log("[LOCAL] unlinked", e)
        local_reset()
        fn_unlinked()
    })
    ws.addEventListener("message", (e)=>{
        local_take_push(e.data)
    })
    ws.addEventListener("error", (e)=>{
        console.log("[LOCAL] websocket error", e)
        local_reset()
        fn_unlinked()
    })
}
function local_set(k,v){
    if (R.is(WebSocket, State.local_ws)) {
        State.local_ws.send(`set-i ${k} ${v}`)
    }
}
function local_fix_msg_unit(a) {
    return a.replaceAll("\\s", " ").replaceAll("\\n", "\n")
}
function local_step(pcur_str, board_str, fn_err) {
    if (! R.is(WebSocket, State.local_ws)) {return fn_err()}
    State.local_ws.send(`step ${pcur_str} ${board_str}`)
    function poll() {
        if (local_usable()) {
            const msg = local_take_poll()
            if (msg.length > 0) {
                const units = msg.split(' ')
                const head = units[0]
                const rest = local_fix_msg_unit(R.drop(1, units).join(' '))
                if (("move" == head) && (units.length > 1)) {
                    return Out.reply("step", units[1])
                } else if (("report" == head) && (units.length > 1)) {
                    Out.reply("status", rest)
                } else if (("say" == head) && (units.length > 1)) {
                    Out.reply("say", rest)
                } else if (("debug" == head) && (units.length > 1)) {
                    console.log("debug", rest)
                } else if ("ready" == head) {
                    if (units.length > 1) {console.log(rest)}
                } else {
                    console.log(`[LOCAL] unknown message: ${msg}`)
                }
            }
            setTimeout(poll, 50)
        } else {
            local_reset()
            fn_err()
        }
    }
    poll()
}

function engine_init(fn_call){
    const msg = sslash_init(fn_call)
    console.log(msg)
    if (State.engine_using == ENGINE_USING_LOCAL) {
        return ENGINE_USING_LOCAL
    } else {
        if (Boolean(msg)) {
            sync_sets(sslash_set)
            return ENGINE_USING_WASM
        } else {
            return ENGINE_USING_JS
        }
    }
}
function engine_set(k,v) {
    if (State.engine_using == ENGINE_USING_LOCAL) {
        local_set(k, v)
    } else if (State.engine_using == ENGINE_USING_WASM) {
        sslash_set(k, v)
    }
}
function engine_gc(){
    if (State.engine_using == ENGINE_USING_LOCAL) {
    } else if (State.engine_using == ENGINE_USING_WASM) {
        sslash_gc()
    } else {
        hashs_gc()
    }
}

const Take = {}

Take["step"] = (pcur_str, board_str)=>{
    const pcur = game.piece_from_str(pcur_str)
    const board = new Board(board_str)
    const n_empty = BOARD_LEN - hashs_n_pieces(board, P_BLACK) - hashs_n_pieces(board, P_WHITE)
    if (n_empty > 0) {
        function oh_my_js() {
            return step(pcur, board, State.n_uct_times, State.n_level)
        }
        function non_local() {
            let moves = ((()=>{
                if (State.engine_using == ENGINE_USING_WASM) {
                    try {
                        return sslash_step(pcur_str, board_str, (str)=>{
                            Out.reply("status", str)
                        }, (str)=>{
                            Out.reply("say", str)
                        })
                    } catch(e) {
                        console.log("wasm throw, switch to pure js", e)
                        State.engine_using = ENGINE_USING_JS
                        return oh_my_js()
                    }
                } else {
                    return oh_my_js()
                }
            })())
            if (moves.length > 0) {
                const hit = moves[rand_range(0, moves.length)]
                Out.reply("step", hit)
            } else {
                console.log("non-move", pcur_str, board_str, board.moves(pcur))
                Out.reply("step pass")
            }
        }
        if (State.engine_using == ENGINE_USING_LOCAL) {
            local_step(pcur_str, board_str, ()=>{
                console.log("local throw, switch to wasm")
                State.engine_using = ENGINE_USING_WASM
                non_local()
            })
        } else {
            non_local()
        }
    } else {
        Out.reply("step pass")
    }
}

Take["set"] = (_k, v)=>{
    function make_fn_num_range(k, min, max, f) {
        return (v)=>{
            const n = Number(v)
            if (n >= min && n <= max) {
                f(n)
                engine_set(k,  n)
                Out.reply_ok("set", k, f())
            }
        }
    }
    const fns = {
        "uct-times": make_fn_num_range("uct-times", N_UCT_TIMES_MIN, N_UCT_TIMES_MAX, (n)=>{
            if (R.is(Number,n)) { State.n_uct_times = n } else { return State.n_uct_times }
        }),
        "level": make_fn_num_range("level", N_LEVEL_MIN, N_LEVEL_MAX, (n)=>{
            if (R.is(Number,n)) { State.n_level = n } else { return State.n_level }
        })
    }
    const k = _k.toLocaleLowerCase()
    if (fns.hasOwnProperty(k)) {
        fns[k](v)
    } else {
        Out.report_error(`set param '${k}' no exist`)
    }
}

Take["used-engine"] = ()=>{
    let name = "JS"
    if (State.engine_using == ENGINE_USING_LOCAL) {name = "LOCAL"}
    else if (State.engine_using == ENGINE_USING_WASM) {name = "WASM"}
    Out.reply("used-engine", name)
}
Take["switch-engine"] = (a)=>{
    const name = String(a).toLocaleUpperCase()
    if ("LOCAL" == name) {
        if (State.engine_using != ENGINE_USING_LOCAL) {
            if (! local_usable()) {
                local_link(State.local_port, ()=>{State.engine_using = ENGINE_USING_LOCAL}, ()=>{State.engine_using = ENGINE_USING_WASM})
            }
        }
    } else if ("WASM" == name) {
        if (State.engine_using != ENGINE_USING_WASM) {
            sslash_sync_sets()
            State.engine_using = ENGINE_USING_WASM
        }
    } else {
        State.engine_using = ENGINE_USING_JS
    }
}
Take["ready-local"] = (a)=>{
    State.local_port = a
}

Take["reset"] = ()=>{
    engine_gc()
    console.log("(reset)")
}

Take["raii"] = ()=>{
    local_reset()
    console.log("(raii)")
}

;(()=>{
    init().then(()=>{
        State.engine_using = engine_init((name,value)=>{
            Out.reply(`ei-${name}`, value)
        })
    })
    self.onmessage = function(e){
        const tokens = e.data.split(' ')
        if (tokens.length > 0){
            const head = tokens[0]
            if (Take.hasOwnProperty(head)) {
                Take[head](...(tokens.slice(1)))
            } else {
                Out.report_error(e.data)
            }
        }
    }
    Out.reply("ready")
})()
